// Tierless
// Copyright © 2010 Alphora
// This file is licensed under a modified BSD-license (see tierless_license.txt).

using System;
using System.Linq;
using System.Runtime.Serialization;

using Tierless.Framework.Data;
using Tierless.Framework.Constraints;

namespace Tierless.Framework.EntityTypes
{
	/// <summary>
	/// Provides a base class for all constraints in the entity model.
	/// </summary>
	[DataContract]
	public abstract class EntityConstraint : NamedEntityTypeObject
	{
		/// <summary>
		/// Initializes a new instance of the EntityConstraint class.
		/// </summary>
		/// <param name="name">The name of the constraint.</param>
		/// <param name="entityTypeName">The name of the entity type on which the constraint is placed.</param>
		/// <param name="columnNames">The names of the columns involved in the constraint.</param>
		/// <param name="severity">The severity of violations of the constraint.</param>
		/// <param name="message">The message used to report violations of the constraint.</param>
		/// <param name="location">The locations at which the constraint can be evaluated.</param>
		/// <param name="transition">The transitions under which the constraint must be evaluated.</param>
		public EntityConstraint
		(
			string name,
			string entityTypeName,
			string[] columnNames,
			ViolationSeverity severity,
			string message,
			ConstraintLocation location,
			ConstraintTransition transition
		)
			: base(name)
		{
			if (String.IsNullOrEmpty(entityTypeName)) throw new ArgumentNullException("AEntityTypeName");
			EntityTypeName = entityTypeName;
			ColumnNames = columnNames ?? new string[0];
			Severity = severity;
			Message = message;
			Location = location;
			Transition = transition;
		}

		/// <summary>
		/// Initializes a new instance of the EntityConstraint class.
		/// </summary>
		/// <param name="name">The name of the constraint.</param>
		/// <param name="entityTypeName">The name of the entity type on which the constraint is placed.</param>
		/// <param name="columnNames">The names of the columns involved in the constraint.</param>
		/// <param name="severity">The severity of violations of the constraint.</param>
		/// <param name="message">The message used to report violations of the constraint.</param>
		public EntityConstraint(string name, string entityTypeName, string[] columnNames, ViolationSeverity severity, string message)
			: this(name, entityTypeName, columnNames, severity, message, ConstraintLocation.Both, ConstraintTransition.InsertUpdate)
		{ }

		/// <summary>
		/// Initializes a new instance of the EntityConstraint class.
		/// </summary>
		/// <param name="name">The name of the constraint.</param>
		/// <param name="entityTypeName">The name of the entity type on which the constraint is placed.</param>
		/// <param name="columnNames">The names of the columns involved in the constraint.</param>
		/// <param name="message">The message used to report violations of the constraint.</param>
		public EntityConstraint(string name, string entityTypeName, string[] columnNames, string message)
			: this(name, entityTypeName, columnNames, ViolationSeverity.Error, message, ConstraintLocation.Both, ConstraintTransition.InsertUpdate)
		{ }

		/// <summary>
		/// Initializes a new instance of the EntityConstraint class.
		/// </summary>
		/// <param name="name">The name of the constraint.</param>
		/// <param name="entityTypeName">The name of the entity type on which the constraint is placed.</param>
		/// <param name="columnNames">The names of the columns involved in the constraint.</param>
		public EntityConstraint(string name, string entityTypeName, string[] columnNames)
			: this
			(
				name,
				entityTypeName,
				columnNames,
				ViolationSeverity.Error,
				String.Format("Violation of constraint '{0}' defined on entity '{1}'.", name, entityTypeName),
				ConstraintLocation.Both,
				ConstraintTransition.InsertUpdate
			)
		{ }

		/// <summary>
		/// Gets the name of the entity type on which the constraint is placed.
		/// </summary>
		[DataMember]
		public string EntityTypeName { get; private set; }

		/// <summary>
		/// Gets the names of the columns involved in the constraint.
		/// </summary>
		[DataMember]
		public string[] ColumnNames { get; private set; }

		/// <summary>
		/// Gets the locations at which the constraint must be evaluated.
		/// </summary>
		[DataMember]
		public ConstraintLocation Location { get; private set; }

		/// <summary>
		/// Gets the transitions under which the constraint must be evaluated.
		/// </summary>
		[DataMember]
		public ConstraintTransition Transition { get; private set; }

		/// <summary>
		/// Gets the message used for violations of the constraint.
		/// </summary>
		[DataMember]
		public string Message { get; private set; }

		/// <summary>
		/// Gets the severity used for violations of the constraint.
		/// </summary>
		[DataMember]
		public ViolationSeverity Severity { get; private set; }

		protected abstract bool IsValid(ConstraintContext context);

		protected virtual ConstraintViolation GetViolation(ConstraintContext context)
		{
			return new ConstraintViolation(Name, EntityTypeName, context.ColumnNames, ColumnNames, context.Severity, context.Message);
		}

		protected virtual string[] GetViolationColumnNames()
		{
			return ColumnNames;
		}

		protected virtual bool ShouldValidate(ConstraintContext context)
		{
			return
				((Transition & context.Transition) != 0)
					&&
					(
						(context.Transition == ConstraintTransition.Delete)
							|| (ColumnNames.Length == 0)
							||
							(
								context.EntityChange.Entity.IsRelevant
									&&
									(
										(context.EntityChange.Columns.Length == 0)
											||
											(
												context.EntityChange.Columns.Any
												(
													AValue => Array.IndexOf(ColumnNames, AValue) >= 0
												)
											)
									)
							)
					);
		}

		protected virtual ConstraintContext CreateContext(IEntityAccessor dataService, EntityChange entityChange, EntitySets entities)
		{
			return
				new ConstraintContext(dataService, entityChange, entities)
					{
							ColumnNames = GetViolationColumnNames(), 
							Severity = Severity, 
							Message = Message
					};
		}

		/// <summary>
		/// Validates the constraint with the given context.
		/// </summary>
		/// <param name="dataService">The data service to be used to access other entities needed during validation.</param>
		/// <param name="entityChange">The entity change that necessitated the validation.</param>
		/// <param name="entities">The set of entities in context.</param>
		/// <returns>A constraint violation if the constraint is violated, null otherwise.</returns>
		public virtual ConstraintViolation Validate(IEntityAccessor dataService, EntityChange entityChange, EntitySets entities)
		{
			var context = CreateContext(dataService, entityChange, entities);
			if (ShouldValidate(context) && !IsValid(context))
				return GetViolation(context);
			return null;
		}

		public override string ToString()
		{
			return String.Format("Constraint '{0}' defined on entity '{1}'", Name, EntityTypeName);
		}
	}

	/// <summary>
	/// Represents a collection of <see cref="EntityConstraint"/>.
	/// </summary>
	public class EntityConstraintCollection : NamedEntityObjectTypeCollection<EntityConstraint>
	{
	}
}